#pragma once 

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <mutex>
#include "util.hpp"
#include "log.hpp"

#define TITLE_WEIGHT 5
#define CONTENT_WEIGHT 1

using std::string;
using std::vector;
using std::unordered_map;

/* index类为单例模式，懒汉版本 */

namespace ns_index
{
    typedef struct document_info
    {
        string title; // 文档标题
        string content; // 文档内容
        string url; // 文档URL
        uint64_t doc_id; // 文档ID，在构建倒排索引时使用
    }doc_info;

    struct inverted_elem 
    {
        uint64_t doc_id; // 文档ID
        string keyword; // 关键词
        int weight; // 权重，当一个关键词对应多个文档时根据权重构建展示顺序
    };

    typedef vector<inverted_elem> inverted_list; // 倒排拉链

    class index
    {
        private:
            vector<doc_info> _forward_index; // 正排索引,下标表示文档ID
            unordered_map<string,inverted_list> _inverted_index; // 倒排索引，一个关键词对应一个倒排拉链（1-n个inverted_elem ）

            static index* _instance; 
            static std::mutex _mtx; 
        private:
            index()
            {

            }

            index(const index& obj) = delete;
            index& operator=(const index& obj) = delete;    

        public:
            ~index()
            {

            }

            static index* GetInstance()
            {
                if(_instance == nullptr)
                {
                    _mtx.lock();
                    if(_instance == nullptr)
                    {
                        _instance = new index();
                    }
                    _mtx.unlock();
                }

                return _instance;
            }

            doc_info* GetForwardIndex(uint64_t doc_id) // 通过文档ID获取文档内容
            {
                if(doc_id >= _forward_index.size())
                {
                    string log_message("document id：");
                    log_message += std::to_string(doc_id);
                    log_message += " out of range";
                    LOG(DEBUG,log_message);

                    //std::cerr << "document id out of range" << std::endl;
                    return nullptr;
                }

                return &_forward_index[doc_id];
            }

            inverted_list* GetInvertedList(const string keyword) // 通过关键词获取倒排拉链
            {
                auto res = _inverted_index.find(keyword);

                if(_inverted_index.end() == res)
                {
                    
                    return nullptr;
                }

                return &(res->second);
            }

            bool BuildIndex(const string& input_file) // 构建正排与倒排索引
            {
                /*
                    1. 二进制方式打开读取去标签与数据清洗后以特定格式存放HTML元素信息的文件
                    2. 每次读取一行数据，对应一个去标签与数据清洗后的HTML文件
                    2.1) 通过数据构建doc_info类型变量，插入到正排索引中
                    2.2) 通过关键词构建倒排拉链，插入到倒排索引中

                    注意：由于会在分词时去掉暂停词，建立倒排索引时速度会很慢
                */
                std::ifstream in(input_file,std::ios::in | std::ios::binary);

                if(!in.is_open())
                {
                    string log_message(input_file);
                    log_message += " open fail";
                    LOG(ERROR,log_message);

                    //std::cerr << input_file << " open fail" << std::endl;

                    return false;
                }

                string line_content;
                while(getline(in,line_content)) 
                {
                    doc_info* elem = BuildForwardIndex(line_content); // 正排索引元素
                    if(nullptr == elem)
                    {
                        continue;
                    }  

                    BuildInvertedIndex(*elem);
                    if(elem->doc_id % 100 == 0)
                    {
                        string log_message("Create index data：");
                        log_message += std::to_string(elem->doc_id);
                        LOG(NORMAL,log_message);    

                        //std::cout << "Create index data：" << elem->doc_id << std::endl;
                    }
                }
                LOG(NORMAL,"Create index data successe");   
                //std::cout << "Create index data successe"  << std::endl;

                return true;
            }

        private:
            doc_info* BuildForwardIndex(const string& line_content)
            {
                /*
                    1. 调用boost::split函数根据分隔符对字符串分割
                    2. 将分割后的结果填充到doc_info类型变量中
                    3. 插入到正排索引中
                */
                #define SEPARATOR '\3' // 分隔符
                vector<string> res; // 字符串分隔后的结果
                /* cut_string函数第三个形参类型为string，需要将字符类型分隔符转换为字符串类型 */
                ns_util::string_util::Split(line_content,&res,string(1,SEPARATOR));
                /* 分隔后res有三个元素,title、content、url */
                if(res.size() != 3)
                {
                    return nullptr;
                }

                doc_info elem;
                elem.title = res[0]; // title
                elem.content = res[1]; // content
                elem.url = res[2]; // url
                elem.doc_id = _forward_index.size(); // 根据下标作为文档ID

                _forward_index.push_back(std::move(elem)); // 提高效率，使用move

                return &_forward_index.back();
            }

            bool BuildInvertedIndex(const doc_info& elem)
            {
                /*
                    1. 通过jieba对elem中title、content分词
                    2. 对分词后的tetle、content进行词频统计
                    3. 根据规则计算inverted_elem中weight,根据关键词更新倒排索引中关键词对应的倒排拉链
                */
                typedef struct keyword_cnt
                {
                    size_t title_cnt;
                    size_t content_cnt;
                }keyword_cnt;

                /* 1. 分词  */
                vector<string> title_keywords;
                vector<string> content_keywords;
                ns_util::jieba_util::CutString(elem.title,&title_keywords);
                ns_util::jieba_util::CutString(elem.content,&content_keywords);

                /* 2. 词频统计 */
                unordered_map<string,keyword_cnt> words_map;
                /*
                   (1) 通过boost::to_lower将关键词转换为小写，倒排索引中存放的都是小写关键词，用户搜索时也需要将搜索词转换为小写
                   (2) unordered_map中对[]运算符进行重载，如果key不存在，则在unordered_map插入一个带有该key的新元素
                       并返回对其映射值的引用
                */
                for(string keyword : title_keywords)
                {
                    boost::to_lower(keyword);
                    words_map[keyword].title_cnt++;
                }

                for(string keyword : content_keywords)
                {
                    boost::to_lower(keyword);
                    words_map[keyword].content_cnt++;
                }

                /* 3. 计算weight,并根据关键词更新倒排索引中关键词对应的倒排拉链 */
                for(auto& keyword : words_map)
                {
                    struct inverted_elem tmp;
                    tmp.doc_id = elem.doc_id;
                    tmp.keyword = keyword.first;
                    tmp.weight =  TITLE_WEIGHT * keyword.second.title_cnt + CONTENT_WEIGHT * keyword.second.content_cnt; 
                    inverted_list& list = _inverted_index[keyword.first];
                    list.push_back(std::move(tmp));
                }

                return true;
            }

    };
    // 静态成员变量初始化
    index* index::_instance = nullptr;
    std::mutex index::_mtx;
}

